﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace Microsoft.AspNetCore.Razor.Language;

internal class LargeTextSourceDocument : RazorSourceDocument
{
    private readonly List<char[]> _chunks;

    private readonly int _chunkMaxLength;

    private readonly RazorSourceLineCollection _lines;

    private readonly int _length;
    private byte[] _checksum;

    public LargeTextSourceDocument(StreamReader reader, int chunkMaxLength, Encoding encoding, RazorSourceDocumentProperties properties)
    {
        if (reader == null)
        {
            throw new ArgumentNullException(nameof(reader));
        }

        if (encoding == null)
        {
            throw new ArgumentNullException(nameof(encoding));
        }

        if (properties == null)
        {
            throw new ArgumentNullException(nameof(properties));
        }

        _chunkMaxLength = chunkMaxLength;
        Encoding = encoding;
        FilePath = properties.FilePath;
        RelativePath = properties.RelativePath;

        ReadChunks(reader, _chunkMaxLength, out _length, out _chunks);
        _lines = new DefaultRazorSourceLineCollection(this);
    }

    public override char this[int position]
    {
        get
        {
            var chunkIndex = position / _chunkMaxLength;
            var insideChunkPosition = position % _chunkMaxLength;

            return _chunks[chunkIndex][insideChunkPosition];
        }
    }

    public override Encoding Encoding { get; }

    public override string FilePath { get; }

    public override int Length => _length;

    public override RazorSourceLineCollection Lines => _lines;

    public override string RelativePath { get; }

    public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
    {
        if (destination == null)
        {
            throw new ArgumentNullException(nameof(destination));
        }

        if (sourceIndex < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(sourceIndex));
        }

        if (destinationIndex < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(destinationIndex));
        }

        if (count < 0 || count > Length - sourceIndex || count > destination.Length - destinationIndex)
        {
            throw new ArgumentOutOfRangeException(nameof(count));
        }

        if (count == 0)
        {
            return;
        }

        var chunkIndex = sourceIndex / _chunkMaxLength;
        var insideChunkPosition = sourceIndex % _chunkMaxLength;
        var remaining = count;
        var currentDestIndex = destinationIndex;

        while (remaining > 0)
        {
            var toCopy = Math.Min(remaining, _chunkMaxLength - insideChunkPosition);
            Array.Copy(_chunks[chunkIndex], insideChunkPosition, destination, currentDestIndex, toCopy);

            remaining -= toCopy;
            currentDestIndex += toCopy;
            chunkIndex++;
            insideChunkPosition = 0;
        }
    }

    public override byte[] GetChecksum()
    {
        if (_checksum == null)
        {
            var charBuffer = new char[Length];
            CopyTo(0, charBuffer, 0, Length);

            var encoder = Encoding.GetEncoder();
            var byteCount = encoder.GetByteCount(charBuffer, 0, charBuffer.Length, flush: true);
            var bytes = new byte[byteCount];
            encoder.GetBytes(charBuffer, 0, charBuffer.Length, bytes, 0, flush: true);

            using (var hashAlgorithm = SHA1.Create())
            {
                _checksum = hashAlgorithm.ComputeHash(bytes);
            }
        }

        var copiedChecksum = new byte[_checksum.Length];
        _checksum.CopyTo(copiedChecksum, 0);

        return copiedChecksum;
    }

    private static void ReadChunks(StreamReader reader, int chunkMaxLength, out int length, out List<char[]> chunks)
    {
        length = 0;
        chunks = new List<char[]>();

        int read;
        do
        {
            var chunk = new char[chunkMaxLength];
            read = reader.ReadBlock(chunk, 0, chunkMaxLength);

            length += read;

            if (read > 0)
            {
                chunks.Add(chunk);
            }
        }
        while (read == chunkMaxLength);
    }
}
